/* HibernateEntityMapper.java
 *
 * Copyright 2006, Tim Dwelle.
 *
 * Licensed under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in
 * compliance with the License.  You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in
 * writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied.  See
 * the License for the specific language governing
 * permissions and limitations under the License.
 *
 */

package dowry.hibernate;

import java.util.*;
import org.hibernate.*;
import org.hibernate.cfg.*;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.tool.hbm2x.*;
import org.hibernate.tool.hbm2x.pojo.*;
import org.hibernate.util.*;
import dowry.*;
import dowry.datatype.*;

/**
 * HibernateEntityMapper maps the Hibernate configuration
 * into a mapping format suitable for Dowry.  In order for
 * this entity mapper to work properly, be sure that the
 * associated datatype manager returns datatypes that
 * implement HibernateType.
 *
 */
public class HibernateEntityMapper implements EntityMapper
{
    public static Map PRIMITIVES = new HashMap();
    static
    {
        PRIMITIVES.put("boolean", "java.lang.Boolean");
        PRIMITIVES.put("byte",    "java.lang.Byte");
        PRIMITIVES.put("char",    "java.lang.Character");
        PRIMITIVES.put("double",  "java.lang.Double");
        PRIMITIVES.put("float",   "java.lang.Float");
        PRIMITIVES.put("int",     "java.lang.Integer");
        PRIMITIVES.put("long",    "java.lang.Long");
        PRIMITIVES.put("short",   "java.lang.Short");
    }

    private Map               augments       = new HashMap();
    private Set               augmentClasses = new HashSet();
    private Set               augmentTypes   = new HashSet();
    private Cfg2JavaTool      cfg2Java       = new Cfg2JavaTool();
    private DatatypeManager   datatypeMgr    = null;
    private HibernateProvider provider       = null;

    /**
     * Augments the entity mapper results with the specified
     * attribute value.  This will override any existing
     * value for this attribute.
     *
     * In this default implementation, this method does
     * nothing.
     *
     * @param entity     the string name of the entity
     *
     * @param property   the string name of the property
     *
     * @param attribute  the string name of the attribute
     *
     * @param value      the attribute value to be included
     *
     */
    public void addAttribute(String entity,
                             String property,
                             String attribute,
                             Object value)
    {
        if (entity != null &&
            property != null &&
            attribute != null)
        {
            Map propertyMap = (Map) augments.get(entity);
            if (propertyMap == null)
            {
                propertyMap = new HashMap();
                augments.put(entity, propertyMap);
            }

            Map attributeMap = (Map) propertyMap.get(property);
            if (attributeMap == null)
            {
                attributeMap = new HashMap();
                propertyMap.put(property, attributeMap);
            }

            if (attribute.equals(AbstractType.JAVA_CLASS) && value != null)
            {
                Class c = getClass(value);
                augmentClasses.add(c);
            }
            else if (attribute.equals(AbstractType.TYPE) && value != null)
            {
                augmentTypes.add(value);
            }

            attributeMap.put(attribute, value);
        }
    }

    /**
     * Returns the classes for the mapped entities.
     *
     * @return  a set of Class objects representing the
     *          mapped entities
     *
     */
    public Set getClasses()
    {
        Set classes = new HashSet();

        Iterator entityIter = getEntitiesIterator();
        while(entityIter.hasNext())
        {
            PersistentClass entity =
                (PersistentClass) entityIter.next();

            if (entity != null)
            {
                String className = entity.getClassName();
                if (className != null)
                {
                    try
                    {
                        Class c = Class.forName(className);
                        classes.add(c);
                    }
                    catch(ClassNotFoundException ex)
                    {
                    }
                }
            }
        }

        classes.addAll(augmentClasses);

        return classes;
    }

    /**
     * Returns the datatype manager to be used by the
     * entity mapper.
     *
     * @return  the datatype manager instance
     *
     */
    public DatatypeManager getDatatypeManager()
    {
        return datatypeMgr;
    }

    /**
     * Returns a mapping of Dowry entities.  The format
     * of this map is:
     *
     * <pre>
     * Map&lt;String entity,
     *     Map&lt;String property,
     *         Map&lt;String attribute, Object value&gt;&gt;&gt;
     *
     * </pre>
     *
     * @return  a map representing the Dowry entities
     *
     */
    public Map getEntityMapping()
    {
        Map entityMap = new HashMap();

        Iterator entityIter = getEntitiesIterator();
        while (entityIter.hasNext())
        {
            PersistentClass entity =
                (PersistentClass) entityIter.next();

            if (entity != null)
            {
                Map propertyMap = new HashMap();
                String entityName = getEntityName(entity);

                Iterator propertyIter = getPropertiesIterator(entity);
                while (propertyIter.hasNext())
                {
                    Property property =
                        (Property) propertyIter.next();

                    if (property != null)
                    {
                        String propertyName = getPropertyName(property);
                        String propertyType = getPropertyType(property);

                        HibernateType datatype =
                            getDatatype(propertyType);

                        if (datatype != null)
                        {
                            Map attributeMap =
                                datatype.toConfiguration(this, property);

                            if (attributeMap != null)
                            {
                                propertyMap.put(propertyName,
                                                attributeMap);
                            }
                        }
                    }
                }

                entityMap.put(entityName, propertyMap);
            }
        }

        // agument mapping with user-specified arguments
        Iterator augmentsIter = augments.keySet().iterator();
        while (augmentsIter.hasNext())
        {
            String entityName = (String) augmentsIter.next();
            Map augmentsPropertyMap = (Map) augments.get(entityName);

            Map propertyMap = (Map) entityMap.get(entityName);
            if (propertyMap == null)
            {
                entityMap.put(entityName, augmentsPropertyMap);
            }
            else
            {
                Iterator propertyIter = augmentsPropertyMap.keySet().iterator();
                while (propertyIter.hasNext())
                {
                    String propertyName = (String) propertyIter.next();
                    Map augmentsAttributeMap =
                        (Map) augmentsPropertyMap.get(propertyName);

                    Map attributeMap = (Map) propertyMap.get(propertyName);
                    if (attributeMap == null)
                    {
                        propertyMap.put(propertyName, augmentsAttributeMap);
                    }
                    else
                    {
                        attributeMap.putAll(augmentsAttributeMap);
                    }
                }
            }
        }

        return entityMap;
    }

    /**
     * Returns the Hibernate provider to be used by the
     * entity mapper.
     *
     * @return  the Hibernate provider instance
     *
     */
    public HibernateProvider getHibernateProvider()
    {
        return provider;
    }

    /**
     * Returns the string names for the mapped entities.
     *
     * @return  a set of the the String names of the mapped
     *          entities
     *
     */
    public Set getNames()
    {
        return getEntityMapping().keySet();
    }

    /**
     * Returns a set of all the string Dowry types
     * referenced in this mapping.  This includes both the
     * types of all mapped properties, as well as the types
     * of the entities themselves.
     *
     * @return a set of String names of the Dowry types
     *         represented in the mapping
     *
     */
    public Set getTypes()
    {
        Set types = new HashSet();

        Iterator entityIter = getEntitiesIterator();
        while(entityIter.hasNext())
        {
            PersistentClass entity =
                (PersistentClass) entityIter.next();

            if (entity != null)
            {
                String type =
                    toType(getEntityType(entity));

                if (type != null)
                {
                    types.add(type);
                }
            }

            Iterator propertyIter =
                getPropertiesIterator(entity);

            while (propertyIter.hasNext())
            {
                Property property =
                    (Property) propertyIter.next();

                if (property != null)
                {
                    String type =
                        toType(getPropertyType(property));

                    if (type != null)
                    {
                        types.add(type);
                    }
                }
            }
        }

        types.addAll(augmentTypes);

        return types;
    }

    /**
     * Sets the datatype manager to be used by the
     * entity mapper.
     *
     * @param datatypeMgr   the datatype manager instance
     *
     */
    public void setDatatypeManager(DatatypeManager datatypeMgr)
    {
        this.datatypeMgr = datatypeMgr;
    }

    /**
     * Sets the Hibernate provider to be used by the
     * entity mapper.
     *
     * @param provider   the Hibernate provider instance
     *
     */
    public void setHibernateProvider(HibernateProvider provider)
    {
        this.provider = provider;
    }

    /**
     * Returns a class object.  It can be either a string or
     * a class.
     *
     * @param value  the object value
     *
     * @return       the class
     *
     */
    protected Class getClass(Object value)
    {
        Class c = null;

        if (value instanceof Class)
        {
            c = (Class) value;
        }
        else
        {
            try
            {
                c = Class.forName(value.toString());
            }
            catch(ClassNotFoundException ex)
            {
            }
        }

        return c;
    }

    /**
     * Returns the Hibernate datatype for the specified
     * classname.  This is just a convenient wrapper to
     * calling getDatatype on the DatatypeManager using
     * a string representing the fully-qualified class
     * name rather than the actual class object.
     *
     * @param  className  the string fully-qualified class
     *                    name
     *
     * @return            the string Dowry type
     *
     */
    protected HibernateType getDatatype(String className)
    {
        HibernateType hibernateType = null;

        DatatypeManager datatypeManager = getDatatypeManager();
        if (className != null && datatypeMgr != null)
        {
            try
            {
                Class c = Class.forName(className);
                Datatype datatype = datatypeManager.getDatatype(c);

                if (datatype != null &&
                    datatype instanceof HibernateType)
                {
                    hibernateType = (HibernateType) datatype;
                }
            }
            catch(ClassNotFoundException ex)
            {
            }
        }

        return hibernateType;
    }

    /**
     * Gets an iterator over all the persistent classes in
     * the Hibernate mapping.
     *
     * <p>
     * For some of our work (like resolving the types of
     * elements inside of a collection), Hibernate requires
     * a session factory.  So even though this method does
     * not use the session factory directly, we will ask the
     * provider for a session factory now.
     * </p>
     *
     * @return        an iterator of Hibernate's persistent
     *                classes
     *
     */
    protected Iterator getEntitiesIterator()
    {
        Iterator iter = null;

        HibernateProvider provider = getHibernateProvider();
        if (provider != null)
        {
            Configuration cfg = provider.getConfiguration();
            SessionFactory sf = provider.getSessionFactory();

            if (cfg != null && sf != null)
            {
                iter = cfg.getClassMappings();
            }
        }

        if (iter == null)
        {
            iter = Collections.EMPTY_LIST.iterator();
        }

        return iter;
    }

    /**
     * Gets the Dowry name for this entity, based on
     * Hibernate's persistent class mapping.  This name
     * should be the Java class name of the peristent
     * class, with the package stripped off.
     *
     * @param entity  the Hibernate persistent class
     *
     * @return        the Dowry name for this entity
     *
     */
    protected String getEntityName(PersistentClass entity)
    {
        String name = null;

        if (entity != null)
        {
            name = StringHelper.unqualify(entity.getClassName());
        }

        return name;
    }

    /**
     * Gets the fully-qualified class name for this entity,
     * based on Hibernate's persistent class mapping.
     *
     * @param entity  the Hibernate persistent class
     *
     * @return        the fully-qualified class name as a
     *                string
     *
     */
    protected String getEntityType(PersistentClass entity)
    {
        String type = null;

        if (entity != null)
        {
            type = entity.getClassName();
        }

        return type;
    }

    /**
     * Gets an iterator over all the properties for the
     * specified Hibernate persistent class.
     *
     * @param entity  the Hibernate persistent class
     *
     * @return        an iterator of the properties of the
     *                specified class
     *
     */
    protected Iterator getPropertiesIterator(PersistentClass entity)
    {
        Iterator iter = null;

        if (entity != null)
        {
            List iterators = new ArrayList();
            while (entity != null)
            {
                EntityPOJOClass epc = new EntityPOJOClass(entity, cfg2Java);
                iterators.add(epc.getAllPropertiesIterator(entity));
                entity = entity.getSuperclass();
            }

            Iterator[] iterArray = (Iterator[]) iterators.toArray(
                new Iterator[iterators.size()] );

            iter = new JoinedIterator(iterArray);
        }
        else
        {
            iter = Collections.EMPTY_LIST.iterator();
        }

        return iter;
    }

    /**
     * Gets the Dowry name for this property, based on
     * Hibernate's property mapping.
     *
     * @param  property  the Hibernate property
     *
     * @return           the Dowry name for this property
     *
     */
    protected String getPropertyName(Property property)
    {
        String name = null;

        if (property != null)
        {
            name = property.getName();
        }

        return name;
    }

    /**
     * Gets the fully-qualified class name for this
     * property, based on Hibernate's persistent class
     * mapping.  This class name will not use the generics
     * notation
     *
     * @param  property  the Hibernate property
     *
     * @return           the fully-qualified class name as a
     *                   string
     *
     */
    protected String getPropertyType(Property property)
    {
        return getPropertyType(property, false);
    }

    /**
     * Gets the fully-qualified class name for this
     * property, based on Hibernate's persistent class
     * mapping.  If useGenerics is true, this class name
     * will use generics style notation (if applicable).
     *
     * @param  property      the Hibernate property
     *
     * @param  useGenerics   indicates whether the class
     *                       name requested should use
     *                       the generics style notation
     *
     * @return               the fully-qualified class name
     *                       as a string
     *
     */
    protected String getPropertyType(Property property, boolean useGenerics)
    {
        String type = null;

        if (property != null)
        {
            type = cfg2Java.getJavaTypeName(property, useGenerics);
            if (PRIMITIVES.containsKey(type))
            {
                type = (String) PRIMITIVES.get(type);
            }
        }

        return type;
    }

    /**
     * Returns the Dowry type for the specified class name.
     *
     * @param  className  the string fully-qualified class
     *                    name
     *
     * @return            the string Dowry type
     *
     */
    protected String toType(String className)
    {
        String type = null;

        if (className != null)
        {
            try
            {
                Class c = Class.forName(className);
                HibernateType datatype = getDatatype(className);
                if (datatype != null)
                {
                    type = datatype.toType(c);
                }
            }
            catch(ClassNotFoundException cnfex)
            {
            }
        }

        return type;
    }
}